/**
 * This file is a part of qloudgen-sec. 
 * You can redistribute qloudgen-sec and/or modify it under the terms of the Lesser GNU General Public License version 3. 
 * qloudgen-sec is distributed WITHOUT ANY WARRANTY. 
 *
 * See the Lesser GNU General Public License for more details at http://www.gnu.org/licenses/. 
 **/
package org.qloudgen.sec.jetty.nosql;

import java.util.Map;
import java.util.Collection;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ConcurrentSkipListSet;

import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpServletRequest;

import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.SessionManager;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.session.AbstractSessionIdManager;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/**
 * <p>Title: Cached session id support</p>
 * <p>Description: Wrapper cached session id manager for jetty cluster implementation.</p>
 * <p>Copyright: qloudgen/sec 2014</p>
 * @author <a href="mailto:hyq.dd.sun@gmail.com">glue.3M</a>
 */
public abstract class AbstractCachedSessionIdManager extends AbstractSessionIdManager {
	/**
	 * Log output.
	 */
	private final static Logger logger = Log.getLogger( AbstractCachedSessionIdManager.class );
	/**
	 * Every 30 minutes.
	 */
	private final static long __defaultScavengePeriod = 30 * 60 * 1000;
	/** 
	 * Purge process is enabled by default.
	 */
	private boolean _purge = true;
	/**
	 * Purge process would run daily by default.
	 */
	private long _purgeDelay = 24 * 60 * 60 * 1000;
	/**
	 * How long do you want to persist sessions that are no longer valid before removing them completely.
	 * Default 1 day
	 */
	protected long _purgeInvalidAge = 24 * 60 * 60 * 1000;
	/**
	 * How long do you want to leave sessions that are still valid before assuming they are dead and removing them.
	 * Default 1 week
	 */
	protected long _purgeValidAge = 7 * 24 * 60 * 60 * 1000;
	/**
	 * Scavenge period.
	 */
	private long _scavengePeriod = __defaultScavengePeriod;
	/**
	 *
	 */
	private Scheduler _scheduler;
	/**
	 *
	 */
	private boolean _ownScheduler;
	/**
	 *
	 */
	private Scheduler.Task _scavengerTask;
	/**
	 *
	 */
	private Scheduler.Task _purgerTask;
	
	/**
	 * Scavenger thread.
	 */
	protected class Scavenger implements Runnable {
		@Override
		public void run() {
			try {
				scavenge();
			}
			finally {
				if ( _scheduler != null && _scheduler.isRunning() ) {
					_scavengerTask = _scheduler.schedule( this , _scavengePeriod , TimeUnit.MILLISECONDS );
				}
			}
		} 
	}
	
	/**
	 * Purger thread.
	 */
	protected class Purger implements Runnable {
		@Override
		public void run() {
			try {
			    purge();
			}
			finally {
				if ( _scheduler != null && _scheduler.isRunning() ) {
					_purgerTask = _scheduler.schedule( this , _purgeDelay , TimeUnit.MILLISECONDS );
				}
			}
		}
	}
	
	/**
	 * Jetty server.
	 */
	private final Server _server;
	/**
	 * Cached session ids.
	 */
	//private final Set<String> _sessionsIds = new HashSet<String>();
	private final Set<String> _sessionsIds = new ConcurrentSkipListSet<String>();
	
	/**
	 * Custom constructor.
	 *
	 * @param server
	 */
	public AbstractCachedSessionIdManager(Server server) {
		// For create session id.
		super( new Random() );
		//
		this._server = server;
	}
	
	/**
	 * Returns current session.
	 *
	 * @param sessionId
	 * @return Map
	 */
	protected abstract Map getSession(String sessionId);
	
	/**
	 * Removes current session.
	 *
	 * @param sessionId
	 * @return Map
	 */
	protected abstract Map removeSession(String sessionId);
	
	/**
	 * Returns all session.
	 *
	 * @return Collections
	 */
	protected abstract Collection<String> getSessionIds();
	
	/**
	 * Returns current purge enabled.
	 *
	 * @return boolean
	 */
	public boolean isPurgeEnabled() {
		return _purge;
	}
    
	/**
	 * Sets current purge enabled.
	 *
	 * @param purge
	 */
	public void setPurge(boolean purge) {
		this._purge = purge;
	}
	
	/**
	 * Returns current purge delay.
	 *
	 * @return long
	 */
	public long getPurgeDelay() {
		return _purgeDelay;
	}
	
    /**
	 * Sets purge delay.
	 *
	 * @param purgeDelay
	 */
	public void setPurgeDelay(long purgeDelay) {
		if ( isRunning() ) {
			throw new IllegalStateException();
		}
		this._purgeDelay = purgeDelay;
	}
	
	/**
	 * Returns current purage invalid age.
	 *
	 * @return long
	 */
	public long getPurgeInvalidAge() {
		return _purgeInvalidAge;
	}
	
	/**
	 * Sets how old a session is to be persisted past the point it is no longer valid.
	 *
	 * @param purgeValidAge
	 */
	public void setPurgeInvalidAge(long purgeInvalidAge) {
		this._purgeInvalidAge = purgeInvalidAge;
	}
	
	/**
	 * Returns current purage valid age.
	 *
	 * @return long
	 */
	public long getPurgeValidAge() {
		return _purgeValidAge;
	}
	
	/**
	 * Sets how old a session is to be persist past the point it is considered no longer viable and should be removed.
	 * 
	 * NOTE: set this value to 0 to disable purging of valid sessions
	 *
	 * @param purgeValidAge
	 */
	public void setPurgeValidAge(long purgeValidAge) {
		this._purgeValidAge = purgeValidAge;
	}
	
	/**
	 * Returns current scavenge period.
	 *
	 * @return long
	 */
	public long getScavengePeriod() {
		return _scavengePeriod;
	}
	
	/**
	 * Sets current period in seconds between scavenge checks.
	 * 
	 * @param scavengePeriod
	 */
	public void setScavengePeriod(long scavengePeriod) {
		if ( scavengePeriod <= 0 ) {
			_scavengePeriod = __defaultScavengePeriod;
		}
		else {
			_scavengePeriod = TimeUnit.SECONDS.toMillis( scavengePeriod );
		}
	}
	
	// ------------------------------------------------------------------------------
	
	/**
	 * Scavenge is a process that periodically checks the tracked session
	 * ids of this given instance of the session id manager to see if they 
	 * are past the point of expiration.
	 */
	protected void scavenge() {
		long now = System.currentTimeMillis();
		//
		logger.info( "Starting scavenge expired session...{}:{}" , _sessionsIds.size() , now );
		//
		//synchronized( _sessionsIds )
		{
			/**
			 * Loop all sessions.
			 *  - are in the known list of sessionIds
			 *  - the expiry time has passed
			 */
			for ( String sessionId : _sessionsIds ) {
				logger.debug( "Current scavenge expired session id is...{}" , sessionId );
				//
				Map session = getSession( sessionId );
				if ( session != null ) {
					logger.debug( "Current scavenge expired session is...{}" , session );
					//
					long expiry = ( Long )session.get( AbstractCachedSessionManager.__EXPIRY );
					if ( expiry > 0 && expiry < now ) {
						expireAll( sessionId );
					}
				}
			}
		}
		logger.info( "Started scavenge expired session...{}" , _sessionsIds.size() );
	}
	
	/**
	 * ScavengeFully will expire all sessions.
	 * In most circumstances you should never need to call this method.
	 * Only for jetty managed operation.
	 */
	protected void scavengeFully() {
		Collection<String> ids = getSessionIds();
		logger.info( "Starting scavenge all session...{}" , ids.size() );
		//
		for ( String sessionId : ids ) {
		    expireAll( sessionId );
		}
		//
		logger.info( "Started scavenge all session...{}" , ids.size() );
	}
	
	/**
	 * Purge is a process that cleans the cache cluster of old sessions that are no longer valid.
	 * 
	 * There are two checks being done here:
	 * 
	 *  - if the accessed time is older than the current time minus the purge invalid age
	 *    and it is no longer valid then remove that session
	 *  - if the accessed time is older then the current time minus the purge valid age
	 *    then we consider this a lost record and remove it
	 *    
	 *  NOTE: if your system supports long lived sessions then the purge valid age should be
	 *  set to zero so the check is skipped.
	 *  
	 *  The second check was added to catch sessions that were being managed on machines 
	 *  that might have crashed without marking their sessions as 'valid=false'
	 */
	protected void purge() {
		Collection<String> ids = getSessionIds();
		logger.info( "Starting purge session...{}" , ids.size() );
		/**
		 * Loop all sessions.
		 */
		for ( String sessionId : ids ) {
			Map session = getSession( sessionId );
			logger.debug( "Current purge sessions is...{}" , session );
			if ( session != null ) {
				//
				long accessed = ( Long )session.get( AbstractCachedSessionManager.__ACCESSED );
				boolean valid = ( Boolean )session.get( AbstractCachedSessionManager.__VALID );
				/**
				 *
				 */
				long invalidAge = System.currentTimeMillis() - _purgeInvalidAge;
				logger.debug( "Current purge session invalid age is...{}" , invalidAge );
				//
				if ( accessed < invalidAge && ! valid ) {
					logger.debug( "Current purge invalid session is...{}" , sessionId );
					removeSession( sessionId );
				}
				/**
				 *
				 */
				if ( _purgeValidAge != 0 ) {
					long validAge = System.currentTimeMillis() - _purgeValidAge;
					logger.debug( "Current purge session valid age is...{}" , validAge );
					//
					if ( accessed < validAge && valid ) {
						logger.debug( "Current purge valid session is...{}" , sessionId );
						removeSession( sessionId );
					}
				}
			}
		}
		//
		logger.info( "Started purge session...{}" , ids.size() );
	}
	
	/**
	 * Purge is a process that cleans the cache cluster of old sessions that are no longer valid.
	 * Only for jetty managed operation.
	 */
	protected void purgeFully() {
		Collection<String> ids = getSessionIds();
		logger.info( "Starting purge all session...{}" , ids.size() );
		/**
		 * Loop all sessions.
		 */
		for ( String sessionId : ids ) {
			Map session = getSession( sessionId );
			logger.debug( "Current purge sessions is...{}" , session );
			if ( session != null ) {
				//
				boolean valid = ( Boolean )session.get( AbstractCachedSessionManager.__VALID );
				if ( ! valid ) {
					logger.debug( "Current purge invalid session is...{}" , sessionId );
					removeSession( sessionId );
				}
			}
		}
		//
		logger.info( "Started purge all session...{}" , ids.size() );
	}

	// ------------------------------------------------------------------------------
	
	/**
	 * Lifecycle for start.
	 */
	@Override
	protected void doStart() throws Exception {
		logger.info( "Starting cached session id manager..." );
		//
		synchronized( this ) {
			/**
			 * Try and use a common scheduler, fallback to own.
			 */
			_scheduler = _server.getBean( Scheduler.class );
			if ( _scheduler == null ) {
				_scheduler = new ScheduledExecutorScheduler();
				_ownScheduler = true;
				_scheduler.start();
			}
			/**
			 * Setup the scavenger thread.
			 */
			if ( _scavengePeriod > 0 ) {
				if ( _scavengerTask != null ) {
					_scavengerTask.cancel();
					_scavengerTask = null;
				}
				// Create scavenger task thread.
				_scavengerTask = _scheduler.schedule( new Scavenger() , _scavengePeriod , TimeUnit.MILLISECONDS );
			}
			/**
			 * If purging is enabled, setup the purge thread.
			 */
			if ( _purge ) {
				if ( _purgerTask != null ) {
					_purgerTask.cancel();
					_purgerTask = null;
				}
				// Create purger task thread.
				_purgerTask = _scheduler.schedule( new Purger() , _purgeDelay , TimeUnit.MILLISECONDS );
			}
		}
		//
		logger.info( "Started cached session id manager..." );
	}
    
	/**
	 * Lifecycle for stop.
	 */
	@Override
	protected void doStop() throws Exception {
		logger.info( "Stopping cached session id manager..." );
		//
		synchronized( this ) {
			/**
			 * Stop scavenger task thread.
			 */
			if ( _scavengerTask != null ) {
				_scavengerTask.cancel();
				_scavengerTask = null;
			}
			/**
			 * Stop purger task thread.
			 */
			if ( _purgerTask != null ) {
				_purgerTask.cancel();
				_purgerTask = null;
			}
			/**
			 * Stop scheduler.
			 */
			if ( _ownScheduler && _scheduler != null ) {
				_scheduler.stop();
				_scheduler = null;
			}
		}
		//
		super.doStop();
		//
		logger.info( "Stopped cached session id manager..." );
	}
	
	// ------------------------------------------------------------------------------
	
	/**
	 * Searches cache to find if the session id known and is it valid.
	 *
	 * @param sessionId
	 * @return boolean
	 */
	@Override
	public boolean idInUse(String sessionId) {
		Map session = getSession( sessionId );
		//
		logger.debug( "Current id in use sessions is...{}" , session );
		//
		if ( session != null ) {
			Boolean valid = ( Boolean )session.get( AbstractCachedSessionManager.__VALID );
			if ( valid == null ) {
				return false;
			}
			//
			return valid;
		}
		//
		return false;
	}
	
	/**
	 * Adds session id to local memory.
	 *
	 * @param session
	 */
	@Override
	public void addSession(HttpSession session) {
		if ( session == null ) {
			logger.debug( "Current add session is null..." );
			return;
		}
		//
		logger.debug( "Current add session is...{}" , session.getId() );
		//
		//synchronized( _sessionsIds )
		{
			_sessionsIds.add( session.getId() );
		}
	}
	
	/**
	 * Removes session id to local memory.
	 *
	 * @param session
	 */
	@Override
	public void removeSession(HttpSession session) {
		if ( session == null ) {
			logger.debug( "Current remove session is null..." );
			return;
		}
		//
		logger.debug( "Current remove session is...{}" , session.getId() );
		//
		//synchronized( _sessionsIds )
		{
			_sessionsIds.remove( session.getId() );
		}
	}
	
	/**
	 * Remove the session id from the list of in-use sessions.
	 * Inform all other known contexts that sessions with the same id should be invalidated.
	 *
	 * @param sessionId
	 */
	@Override
	public void invalidateAll(String sessionId) {
		logger.info( "Starting invalidate session from all contexts by...{}" , sessionId );
		//
		//synchronized( _sessionsIds )
		{
			/**
			 * Force remove current session id from local memory.
			 */
			_sessionsIds.remove( sessionId );			
			/**
			 * Second tell all contexts that may have a session object with this id to get rid of them.
			 */
			Handler[] contexts = _server.getChildHandlersByClass( ContextHandler.class );
			for ( int i=0 ; contexts != null && i < contexts.length ; i++ ) {
				logger.debug( "Current context handler is...{}" , contexts[i] );
				//
				SessionHandler sessionHandler = ( ( ContextHandler )contexts[i] ).getChildHandlerByClass( SessionHandler.class );
				if ( sessionHandler != null ) {
					SessionManager sessionManager = sessionHandler.getSessionManager();
					//
					logger.debug( "Current session handler is...{}" , sessionHandler );
					logger.debug( "Current session manager is...{}" , sessionManager );
					//
					if ( sessionManager != null && sessionManager instanceof AbstractCachedSessionManager ) {
						( ( AbstractCachedSessionManager )sessionManager ).invalidateSession( sessionId );
					}
				}
			}
		}
		//
		logger.info( "Started invalidate session from all contexts by...{}" , sessionId );
	}
	
	/**
	 * Renew this session id for all contexts that are sharing the session id.
	 *
	 * @param oldClusterId
	 * @param oldNodeId
	 * @param request
	 */
	@Override
	public void renewSessionId(String oldClusterId , String oldNodeId , HttpServletRequest request) {
		logger.info( "Starting renew session id from all contexts by...{}-->{}" , oldClusterId , oldNodeId );
		/**
		 * Generate a new id.
		 */
		String newClusterId = newSessionId( request.hashCode() );
		String newNodeId = getNodeId( newClusterId , request );
		//
		//synchronized( _sessionsIds )
		{
			/**
			 * Force remove current session id from local memory.
			 */
			_sessionsIds.remove( oldClusterId );
			/**
			 * Force add current renew session id to local memory.
			 */
			_sessionsIds.add( newClusterId );
			/**
			 * Second tell all contexts that may have a session object with this id to get rid of them.
			 */
			Handler[] contexts = _server.getChildHandlersByClass( ContextHandler.class );
			for ( int i=0 ; contexts != null && i < contexts.length ; i++ ) {
				logger.debug( "Current context handler is...{}" , contexts[i] );
				//
				SessionHandler sessionHandler = ( ( ContextHandler )contexts[i] ).getChildHandlerByClass( SessionHandler.class );
				if ( sessionHandler != null ) {
					SessionManager sessionManager = sessionHandler.getSessionManager();
					//
					logger.debug( "Current session handler is...{}" , sessionHandler );
					logger.debug( "Current session manager is...{}" , sessionManager );
					//
					if ( sessionManager != null && sessionManager instanceof AbstractCachedSessionManager ) {
						( ( AbstractCachedSessionManager )sessionManager ).renewSessionId( oldClusterId , oldNodeId , newClusterId , newNodeId );
					}
				}
			}
		}
		//
		logger.info( "Started renew session id from all contexts by...{}-->{}" , newClusterId , newNodeId );
	}

	/**
	 * Expire this session for all contexts that are sharing the session id.
	 *
	 * @param sessionId
	 */
	public void expireAll(String sessionId) {
		logger.info( "Starting expired session from all contexts by...{}" , sessionId );
		//
		//synchronized( _sessionsIds )
		{
			/**
			 * Force remove current session id from local memory.
			 */
			_sessionsIds.remove( sessionId );
			/**
			 * Second tell all contexts that may have a session object with this id to get rid of them.
			 */
			Handler[] contexts = _server.getChildHandlersByClass( ContextHandler.class );
			for ( int i=0 ; contexts != null && i < contexts.length ; i++ ) {
				logger.debug( "Current context handler is...{}" , contexts[i] );
				//
				SessionHandler sessionHandler = ( ( ContextHandler )contexts[i] ).getChildHandlerByClass( SessionHandler.class );
				if ( sessionHandler != null ) {
					SessionManager sessionManager = sessionHandler.getSessionManager();
					//
					logger.debug( "Current session handler is...{}" , sessionHandler );
					logger.debug( "Current session manager is...{}" , sessionManager );
					//
					if ( sessionManager != null && sessionManager instanceof AbstractCachedSessionManager ) {
						( ( AbstractCachedSessionManager )sessionManager ).expire( sessionId );
					}
				}
			}
		}
		//
		logger.info( "Started expired session from all contexts by...{}" , sessionId );
	}
}